<?php

class AbstractRepository
{

  /**
   *
   * @var array
   */
  protected static $instances = array();

  /**
   *
   * @var IDomObjDefinition
   */
  protected $def;

  /**
   *
   * @param string $name
   * @return AbstractRepository
   */
  public static function getInstance($name)
  {
    if (empty(self::$instances[$name])) {
      self::$instances[$name] = new self($name);
    }

    return self::$instances[$name];
  }

  /**
   *
   * @param string $name
   * @return void
   */
  protected function __construct($name)
  {
    $this->def = Domain::getInstance()->getObject($name);
  }

  /**
   *
   * @param $name
   * @param $data
   * @return unknown_type
   */
  public function delete($data)
  {
    $def = $this->def;

    try {
      $def = Domain::toAggregate($def);
      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        if (isset($data[$assoc_name])) {
          $def->getAssociation($assoc_name)->getDAO()->beforeDelete($data);
        }
      }
    }
    catch (DomainTypeConversionException $e) {
      //
    }

    $def = Domain::toEntity($def);

    $table = $def->getTable();
    $pk = $def->getPkName();

    $id = $data[$pk];

    $dao = DAO::getInstance();

    $query = $dao->query("DELETE FROM $table WHERE $pk = ?");

    $result = $query->execute($id);

    try {
      $def = Domain::toAggregate($def);
      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        if (isset($data[$assoc_name])) {
          $def->getAssociation($assoc_name)->getDAO()->afterDelete($data);
        }
      }
    }
    catch (DomainTypeConversionException $e) {
      //
    }
  }

  /**
   *
   * @param IDomObjDefinition $def
   * @param array $data
   * @return void
   */
  public function save(&$data)
  {
    $def = $this->def;

    try {
      $def = Domain::toAggregate($def);
      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        $def->getAssociation($assoc_name)->getDAO()->beforeSave($data);
      }
    }
    catch (DomainTypeConversionException $e) {
      //
    }
    catch (AssociationNotFoundException $e) {
      //
    }

    $def = Domain::toEntity($def);
    if (empty($data[$def->getPkName()])) {
      $this->_insert($def, $data);
    }
    else {
      $this->_update($def, $data);
    }

    try {
      $def = Domain::toAggregate($def);
      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        $def->getAssociation($assoc_name)->getDAO()->afterSave($data);
      }
    }
    catch (DomainTypeConversionException $e) {
      //
    }
    catch (AssociationNotFoundException $e) {
      //
    }
  }

  /**
   *
   * @param IDomObjDefinition $def
   * @param array $data
   * @return array
   */
  protected function _insert($def, &$data)
  {
    $def = Domain::toEntity($def);

    $_data = self::collectData($def, $data);

    $table = $def->getTable();
    $pk = $def->getPkName();

    $dao = DAO::getInstance();

    $sql = "INSERT INTO $table " . QueryObject::buildInsert($_data, '?');
    $query = $dao->query($sql);

    $result = $query->execute(array_values($_data));

    $data[$pk] = $dao->lastInsertId();
  }

  /**
   *
   * @param IDomObjDefinition $def
   * @param array $data
   * @return void
   */
  protected function _update($def, &$data)
  {
    $def = Domain::toEntity($def);

    $_data = self::collectData($def, $data);

    $table = $def->getTable();
    $pk = $def->getPkName();
    $id = $_data[$pk];

    unset($_data[$pk]);

    if (! empty($_data)) {
      $dao = DAO::getInstance();

      $query = $dao->query("UPDATE $table " . QueryObject::buildUpdate($_data, '?') . " WHERE $pk = ?");

      $_data[] = $id;

      $result = $query->execute(array_values($_data));
    }
  }

  public function __call($name, array $arguments)
  {
    if (strpos($name, 'findBy') === 0) {
      $field = substr($name, 6);

      array_unshift($arguments, $field);

      return call_user_func_array(array($this, '_findBy'), $arguments);
    }
    elseif (strpos($name, 'findAllBy') === 0) {
      $field = substr($name, 9);

      array_unshift($arguments, $field);

      return call_user_func_array(array($this, '_findAllBy'), $arguments);
    }
    elseif (strpos($name, 'findCountBy') === 0) {
      $field = substr($name, 11);

      array_unshift($arguments, $field);

      return call_user_func_array(array($this, '_findCountBy'), $arguments);
    }
    elseif (strpos($name, 'findCount') === 0) {
      $field = substr($name, 9);

      array_unshift($arguments, $field);

      return call_user_func_array(array($this, '_findCount'), $arguments);
    }
    elseif (strpos($name, 'findAll') === 0) {
      $field = substr($name, 7);

      array_unshift($arguments, $field);

      return call_user_func_array(array($this, '_findAll'), $arguments);
    }
    elseif (strpos($name, 'find') === 0) {
      $field = substr($name, 4);

      array_unshift($arguments, $field);

      return call_user_func_array(array($this, '_find'), $arguments);
    }
  }

  public function findCount($params = array())
  {
    $def = Domain::toEntity($this->def);

    $table = $def->getTable();
    $pk = $def->getPkName();

    $query = DAO::getInstance()->query()->from($table)->select("COUNT($pk)");

    $this->setOrderBy($query, $params);
    $this->setLimit($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute(sy_get_param($params, 'data'));

    $data = $result->fetchOne();

    return $data;
  }

  protected function _findCountBy($field, $name, $value, $params = array())
  {
    $def = Domain::toEntity(Domain::getInstance()->getObject($name));

    $table = $def->getTable();
    $pk = $def->getPkName();
    $key = $def->getField($field)->getName();

    $query = DAO::getInstance()->query()->from($table)->select("COUNT($pk)")->where("$key = ?");

    $this->setOrderBy($query, $params);
    $this->setLimit($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute($value);

    $data = $result->fetchOne();

    return $data;
  }

  protected function _find($params = array())
  {
    $params = array_merge(array('depth' => 0), ( array ) $params);

    $def = Domain::toEntity($this->def);

    $table = $def->getTable();

    $query = DAO::getInstance()->query()->from($table);

    $this->setFields($query, $params);
    $this->setOrderBy($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute(sy_get_param($params, 'data'));

    $data = null;

    if ($result->numRows()) {
      $data = $result->fetchRow();

      $depth = sy_get_param($params, 'depth', 0);

      if ($depth != 0) {
        $this->intr($def, $data, $depth - 1);
      }
    }

    return $data;
  }

  protected function _findBy($field, $value, $params = array())
  {
    $params = array_merge(array('depth' => 0), ( array ) $params);

    $def = Domain::toEntity($this->def);

    $table = $def->getTable();
    $key = $def->getField($field)->getName();

    $query = DAO::getInstance()->query()->from($table)->where("$key = ?");

    $this->setFields($query, $params);
    $this->setOrderBy($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute($value);

    $data = $result->fetchRow();

    $depth = sy_get_param($params, 'depth', 0);

    if ($depth != 0) {
      $this->intr($def, $data, $depth - 1);
    }

    return $data;
  }

  protected function _findAll($params = array())
  {
    $params = array_merge(array('depth' => 0), ( array ) $params);

    $def = Domain::toEntity($this->def);

    $table = $def->getTable();

    $query = DAO::getInstance()->query()->from($table);

    $this->setFields($query, $params);
    $this->setOrderBy($query, $params);
    $this->setLimit($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute(sy_get_param($params, 'data'));

    $data = $result->fetchAll();

    $depth = sy_get_param($params, 'depth', 0);

    if ($depth != 0) {
      $this->intr($def, $data, $depth - 1);
    }

    return $data;
  }

  protected function _findAllBy($field, $value, $params = array())
  {
    $params = array_merge(array('depth' => 0), ( array ) $params);

    $def = Domain::toEntity($this->def);

    $table = $def->getTable();
    $key = $def->getField($field)->getName();

    $query = DAO::getInstance()->query()->from($table)->where("$key = ?");

    $this->setFields($query, $params);
    $this->setOrderBy($query, $params);
    $this->setLimit($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute($value);

    $data = $result->fetchAll();

    $depth = sy_get_param($params, 'depth', 0);

    if ($depth != 0) {
      $this->intr($def, $data, $depth - 1);
    }

    return $data;
  }

  protected function intr($def, &$data, $depth = 0)
  {
    if ($depth <= - 255) {
      throw new Exception("Maximum recursions reached");
    }

    if (empty($data)) {
      return;
    }

    $def = Domain::toAggregate($def);

    foreach ($def->getAssociations() as $assoc_name => $assoc) {
      $assoc = $def->getAssociation($assoc_name);

      $assoc->getDAO()->find($data);

      $k = $def->getPkName();

      if ($depth != 0) {
        if (isset($data[$k])) {
          $this->intr($assoc->getTarget(), $data[$assoc_name], $depth - 1);
        }
        else {
          $m = array();

          foreach ($data as &$row) {
            if (empty($row[$assoc_name])) {
              continue;
            }
            elseif (isset($row[$assoc_name][$k])) {
              $m[] = &$row;
            }
            elseif (isset($row[$assoc_name][$assoc->getForeignKeyField()->getName()])) {
              $m[] = &$row[$assoc_name];
            }
            else {
              foreach ($row[$assoc_name] as &$child) {
                $m[] = &$child;
              }
            }
          }

          $this->intr($assoc->getTarget(), $m, $depth - 1);
        }
      }
    }
  }

  protected function setFields(IQueryObject $query, array $params) //, IEntityDefinition $def)
  {
    if (isset($params['fields'])) {
      $query->select($params['fields']);
    }
    else {
      $query->select(AbstractRepository::collectFields($this->def));
    }
  }

  protected function setOrderBy(IQueryObject $query, array $params)
  {
    if (isset($params['orderBy'])) {
      $query->orderBy($params['orderBy']);
    }
  }

  protected function setLimit(IQueryObject $query, array $params)
  {
    if (isset($params['limit'])) {
      $query->limit($params['limit']);
    }
  }

  protected function setOffset(IQueryObject $query, array $params)
  {
    if (isset($params['offset'])) {
      $query->offset($params['offset']);
    }
  }

  protected function setWhere(IQueryObject $query, array $params)
  {
    if (isset($params['where'])) {
      if (is_array($params['where'])) {
        foreach ($params['where'] as $where) {
          $query->where($where);
        }
      }
      else {
        $query->where($params['where']);
      }
    }
  }

  public static function collectFields(IEntityDefinition $def, $addTable = true)
  {
    $fields = array();

    foreach ($def->getFields() as $name => $field) {
      $field = $def->getField($name);
      $table = $field->getTable();
      $fields[] = ($addTable ? ($table ? $table : $def->getTable()) . '.' : '') . $field->getName();
    }

    return $fields;
  }

  public static function collectData(IEntityDefinition $def, $data)
  {
    $fields = self::collectFields($def, false);
    $fields = array_flip($fields);
    return array_intersect_key($data, $fields);
  }

}

?>